/*
 * Copyright 2002-2013 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.springframework.security.config.annotation.authentication.configurers.provisioning;

import java.util.ArrayList;
import java.util.List;

import javax.sql.DataSource;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.security.config.annotation.authentication.ProviderManagerBuilder;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.provisioning.JdbcUserDetailsManager;

/**
 * Configures an {@link org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder} to
 * have JDBC authentication. It also allows easily adding users to the database used for authentication and setting up
 * the schema.
 *
 * <p>
 * The only required method is the {@link #dataSource(javax.sql.DataSource)} all other methods have reasonable defaults.
 * </p>
 *
 * @param <B> the type of the {@link ProviderManagerBuilder} that is being configured
 *
 * @author Rob Winch
 * @since 3.2
 */
public class JdbcUserDetailsManagerConfigurer<B extends ProviderManagerBuilder<B>> extends
        UserDetailsManagerConfigurer<B,JdbcUserDetailsManagerConfigurer<B>> {

    private DataSource dataSource;

    private List<Resource> initScripts = new ArrayList<Resource>();

    public JdbcUserDetailsManagerConfigurer(JdbcUserDetailsManager manager) {
        super(manager);
    }

    public JdbcUserDetailsManagerConfigurer() {
        this(new JdbcUserDetailsManager());
    }


    /**
     * Populates the {@link DataSource} to be used. This is the only required attribute.
     *
     * @param dataSource the {@link DataSource} to be used. Cannot be null.
     * @return
     * @throws Exception
     */
    public JdbcUserDetailsManagerConfigurer<B> dataSource(DataSource dataSource) throws Exception {
        this.dataSource = dataSource;
        getUserDetailsService().setDataSource(dataSource);
        return this;
    }

    /**
    * Sets the query to be used for finding a user by their username. For example:
    *
    * <code>
    *     select username,password,enabled from users where username = ?
    * </code>
    * @param query  The query to use for selecting the username, password, and if the user is enabled by username.
    *               Must contain a single parameter for the username.
    * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations
    * @throws Exception
    */
    public JdbcUserDetailsManagerConfigurer<B> usersByUsernameQuery(String query) throws Exception {
        getUserDetailsService().setUsersByUsernameQuery(query);
        return this;
    }

    /**
     * Sets the query to be used for finding a user's authorities by their username. For example:
     *
     * <code>
     *     select username,authority from authorities where username = ?
     * </code>
     *
     * @param query  The query to use for selecting the username, authority  by username.
     *               Must contain a single parameter for the username.
     * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations
     * @throws Exception
     */
    public JdbcUserDetailsManagerConfigurer<B> authoritiesByUsernameQuery(String query) throws Exception {
        getUserDetailsService().setAuthoritiesByUsernameQuery(query);
        return this;
    }

    /**
     * An SQL statement to query user's group authorities given a username. For example:
     *
     * <code>
     *     select
     *         g.id, g.group_name, ga.authority
     *     from
     *         groups g, group_members gm, group_authorities ga
     *     where
     *         gm.username = ? and g.id = ga.group_id and g.id = gm.group_id
     * </code>
     *
     * @param query  The query to use for selecting the authorities by group.
     *               Must contain a single parameter for the username.
     * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations
     * @throws Exception
     */
    public JdbcUserDetailsManagerConfigurer<B> groupAuthoritiesByUsername(String query) throws Exception {
        JdbcUserDetailsManager userDetailsService = getUserDetailsService();
        userDetailsService.setEnableGroups(true);
        userDetailsService.setGroupAuthoritiesByUsernameQuery(query);
        return this;
    }

    /**
     * A non-empty string prefix that will be added to role strings loaded from persistent storage (default is "").
     *
     * @param rolePrefix
     * @return
     * @throws Exception
     */
    public JdbcUserDetailsManagerConfigurer<B> rolePrefix(String rolePrefix) throws Exception {
        getUserDetailsService().setRolePrefix(rolePrefix);
        return this;
    }


    /**
     * Defines the {@link UserCache} to use
     *
     * @param userCache the {@link UserCache} to use
     * @return the {@link JdbcUserDetailsManagerConfigurer} for further customizations
     * @throws Exception
     */
    public JdbcUserDetailsManagerConfigurer<B> userCache(UserCache userCache) throws Exception {
        getUserDetailsService().setUserCache(userCache);
        return this;
    }

    @Override
    protected void initUserDetailsService() throws Exception {
        if(!initScripts.isEmpty()) {
            getDataSourceInit().afterPropertiesSet();
        }
        super.initUserDetailsService();
    }

    @Override
    public JdbcUserDetailsManager getUserDetailsService() {
        return (JdbcUserDetailsManager) super.getUserDetailsService();
    }

    /**
     * Populates the default schema that allows users and authorities to be stored.
     *
     * @return The {@link JdbcUserDetailsManagerRegistry} used for additional customizations
     */
    public JdbcUserDetailsManagerConfigurer<B> withDefaultSchema() {
        this.initScripts.add(new ClassPathResource("org/springframework/security/core/userdetails/jdbc/users.ddl"));
        return this;
    }

    protected DatabasePopulator getDatabasePopulator() {
        ResourceDatabasePopulator dbp = new ResourceDatabasePopulator();
        dbp.setScripts(initScripts.toArray(new Resource[initScripts.size()]));
        return dbp;
    }

    private DataSourceInitializer getDataSourceInit() {
        DataSourceInitializer dsi = new DataSourceInitializer();
        dsi.setDatabasePopulator(getDatabasePopulator());
        dsi.setDataSource(dataSource);
        return dsi;
    }
}
